#include <node.h>
#include <v8.h>
#include <stdexcept>
#include "fft.h"
#include "decoder.h"
#include "timing.h"

using namespace std;
using namespace v8;


#ifdef _MSC_VER
inline float getNan() { float ret; unsigned int i = 0x7fc00000U; memcpy(&ret, &i, 4); return ret; }
#define NAN (getNan())
#endif


// 指定发生错误（如参数错误、加载失败、采样率不支持）时是否抛出js异常
// timing无法算出时不会抛出异常，会返回空结果
#define THROW_EXCEPTION_ON_ERROR 0


void Method_Load(const FunctionCallbackInfo<Value> & args);
void Method_Unload(const FunctionCallbackInfo<Value> & args);
void Method_Analyze(const FunctionCallbackInfo<Value> & args);

void init(Handle<Object> target) {
	initFftTables();

	/*
	 * 读取音频，并提取特征用于timing分析
	 * 在unload或下次load前调用analyze，都是对本次load的音频分析
	 * load完成后不会占用音频文件，只是把音频特征保留在内存中
	 * 参数: filename
	 *     String filename: 音频文件文件名
	 * 返回值: Int32 音频长度(ms)，失败时返回抛异常或返回0（看上面#define）
	 */
	NODE_SET_METHOD(target, "load", Method_Load);
	NODE_SET_METHOD(target, "unload", Method_Unload);
	/*
	 * 分析timing，需要先load
	 * 参数: bpm = NaN, start = NaN, end = NaN
	 *     Number bpm:        可选，不计算bpm，直接用给定的bpm计算offset。不填或输入NaN时计算bpm和offset
	 *     Number start, end: 【未实现】可选，指定用于timing分析的时间范围(ms)。不填或输入NaN时表示从头/到尾
	 * 返回值: Object 计算结果
	 *     Int32  bpm:               BPM，会自动舍入到附近的整值（包括0.5等）。无法算出时为NaN，参数给定bpm时复制到结果中
	 *     Int32  offset:            offset，表示音频中(60000/BPM) * N + offset的位置是拍点。无法算出时为NaN
	 *     Number rawBpm:            未取整的原始bpm结果.无法算出时为NaN。参数给定bpm时复制到结果中
	 *     Number rawBpmUncertainty: 原始bpm结果的不确定度(1σ)。参数给定bpm或无法算出时为NaN
	 *     Int32  signature:         【实验功能】每小节拍数。参数给定bpm或无法算出时为0
	 *     Int32  division:          【实验功能】每拍细分数。参数给定bpm或无法算出时为0
	 */
	NODE_SET_METHOD(target, "analyze", Method_Analyze);
}

NODE_MODULE(binding, init);


vector<float> feature;


void loadAudio(const char * filename) {
	unsigned sampleRate;
	vector<float> audio = decodeFmod(filename, sampleRate);
	feature = preprocess(audio, sampleRate);
}


void unloadAudio() {
	feature.clear();
	feature.shrink_to_fit();
}


void Method_Load(const FunctionCallbackInfo<Value> & args) {
	Isolate * isolate = Isolate::GetCurrent();
	HandleScope scope(isolate);

	unloadAudio();

	if (args.Length() < 1 || !args[0]->IsString()) {
#if THROW_EXCEPTION_ON_ERROR
		isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong arguments")));
#else
		args.GetReturnValue().Set(Int32::New(isolate, 0));
#endif
		return;
	}
	String::Utf8Value filename(args[0]->ToString());

	try {
		loadAudio(*filename);
	} catch (exception & e) {
#if THROW_EXCEPTION_ON_ERROR
		isolate->ThrowException(Exception::Error(String::NewFromUtf8(isolate, e.what())));
		return;
#endif
	}

	args.GetReturnValue().Set(Int32::New(isolate, feature.size()));
}


void Method_Unload(const FunctionCallbackInfo<Value> & args) {
	unloadAudio();
}


void Method_Analyze(const FunctionCallbackInfo<Value> & args) {
	Isolate * isolate = Isolate::GetCurrent();
	HandleScope scope(isolate);
	Local<Object> ret = Object::New(isolate);

	bool bpmGiven = false;
	double bpm;

	if (args.Length() >= 1) {
		if (!args[0]->IsNumber()) {
#if THROW_EXCEPTION_ON_ERROR
			isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong arguments")));
#else
			ret->Set(String::NewFromUtf8(isolate, "bpm"), Number::New(isolate, NAN));
			ret->Set(String::NewFromUtf8(isolate, "rawBpm"), Number::New(isolate, NAN));
			ret->Set(String::NewFromUtf8(isolate, "rawBpmUncertainty"), Number::New(isolate, NAN));
			ret->Set(String::NewFromUtf8(isolate, "signature"), Int32::New(isolate, 0));
			ret->Set(String::NewFromUtf8(isolate, "division"), Int32::New(isolate, 0));
			ret->Set(String::NewFromUtf8(isolate, "offset"), Number::New(isolate, NAN));
			args.GetReturnValue().Set(ret);
#endif
			return;
		}
		bpm = args[0]->NumberValue();
		if (bpm == bpm && bpm > 1.0 && bpm < 6000.0) {
			bpmGiven = true;
			ret->Set(String::NewFromUtf8(isolate, "bpm"), Number::New(isolate, bpm));
			ret->Set(String::NewFromUtf8(isolate, "rawBpm"), Number::New(isolate, bpm));
			ret->Set(String::NewFromUtf8(isolate, "rawBpmUncertainty"), Number::New(isolate, NAN));
			ret->Set(String::NewFromUtf8(isolate, "signature"), Int32::New(isolate, 0));
			ret->Set(String::NewFromUtf8(isolate, "division"), Int32::New(isolate, 0));
		}
	}

	if (!bpmGiven) {
		double rawBpm;
		double uncertainty;
		unsigned signature;
		unsigned division;
		int bpmRet = calcBpm(feature, rawBpm, uncertainty, signature, division);
		if (bpmRet >= 0) {
			if (bpmRet < 16) {
				bpm = snapBpm(rawBpm, uncertainty);
			} else {
				bpm = rawBpm;
			}
			ret->Set(String::NewFromUtf8(isolate, "bpm"), Number::New(isolate, bpm));
			ret->Set(String::NewFromUtf8(isolate, "rawBpm"), Number::New(isolate, rawBpm));
			ret->Set(String::NewFromUtf8(isolate, "rawBpmUncertainty"), Number::New(isolate, uncertainty));
			ret->Set(String::NewFromUtf8(isolate, "signature"), Int32::New(isolate, signature));
			ret->Set(String::NewFromUtf8(isolate, "division"), Int32::New(isolate, division));
		} else {
			ret->Set(String::NewFromUtf8(isolate, "bpm"), Number::New(isolate, NAN));
			ret->Set(String::NewFromUtf8(isolate, "rawBpm"), Number::New(isolate, NAN));
			ret->Set(String::NewFromUtf8(isolate, "rawBpmUncertainty"), Number::New(isolate, NAN));
			ret->Set(String::NewFromUtf8(isolate, "signature"), Int32::New(isolate, 0));
			ret->Set(String::NewFromUtf8(isolate, "division"), Int32::New(isolate, 0));
			ret->Set(String::NewFromUtf8(isolate, "offset"), Number::New(isolate, NAN));
			args.GetReturnValue().Set(ret);
			return;
		}
	}

	double offset;
	int offsetRet = calcOffset(feature, bpm, offset);
	if (offsetRet >= 0) {
		ret->Set(String::NewFromUtf8(isolate, "offset"), Number::New(isolate, offset));
	} else {
		ret->Set(String::NewFromUtf8(isolate, "offset"), Number::New(isolate, NAN));
	}

	args.GetReturnValue().Set(ret);
}
